Skip to content

feat: table view with sorting, faceted filters, and bulk delete for templates, workflows, and campaigns#396

Open
taniasanz7 wants to merge 4 commits into
useplunk:nextfrom
taniasanz7:patch-23-tanstack-table
Open

feat: table view with sorting, faceted filters, and bulk delete for templates, workflows, and campaigns#396
taniasanz7 wants to merge 4 commits into
useplunk:nextfrom
taniasanz7:patch-23-tanstack-table

Conversation

@taniasanz7
Copy link
Copy Markdown
Contributor

@taniasanz7 taniasanz7 commented May 31, 2026

Description

Adds an opt-in table view to the Templates, Workflows, and Campaigns list pages, built on @tanstack/react-table and styled to match the existing shadcn/Radix components. The card grid becomes unusable past a few dozen rows; this gives a scannable, sortable, filterable table for large accounts.

Card view remains the default and is unchanged - the table is opt-in via a per-page card/table switcher (persisted in localStorage), so existing users see no difference unless they switch.

Table view adds:

  • Click-to-sort column headers (asc → desc → unsorted). The server is authoritative via new ?sort=&dir= query params (name / createdAt / updatedAt, plus
    steps on workflows by relation count)
  • Per-column faceted filters (Excel-style dropdowns in the
    header) for fixed-value columns: template Type, campaign Status, workflow Status (active/disabled). Free-text stays in the existing search box.
  • Column visibility menu, persisted per page.
  • Bulk delete with a checkbox column, shift-click range selection, and a selection action bar. Each resource keeps its own single-delete guard: templates referenced by a workflow step abort the batch (409), workflows with active executions abort (409), campaigns must be DRAFT (400). All checks + the deleteMan y run in one transaction, so partial deletes are impossible (max 1000 ids/request).
  • A "no results match your filters" empty state with a Clear filters button, distinct from the genuine first-run empty state.

Backend: a small shared parseListSort helper adds ?sort=&dir= to the three list endpoints; POST /{templates,workflows,cam paigns}/bulk-update accepts { ids, delete? }. All additive —
existing API shapes and defaults are unchanged. No schema migration steps on workflows by relation count); the client only mirrors sort state into the request.

  • Per-column faceted filters (Excel-style dropdowns in the
    header) for fixed-value columns: template Type, campaign Status, workflow Status (active/disabled). Free-text stays in the existing search box.
  • Column visibility menu, persisted per page.
  • Bulk delete with a checkbox column, shift-click range selection

Testing

  • tsc --noEmit clean across apps/api and apps/web
  • next build succeeds; /templates, /workflows, /campaigns prerender
  • Vitest (per-service): WorkflowService 56/56 (incl. new enabled-status filter + step-count sort cases), TemplateService 42/42
    and CampaignService 32/32 (incl. bulk-delete: project-scope 404, guard 409/400 rollback, dedup, no-op)
  • Manually verified on a self-hosted instance with ~200 templat
    es / ~400 campaigns: switcher, header sort, faceted filters, column visibility, and bulk delete with shift-select; confirmed card view is unchanged

Checklist

  • PR title follows conventional commits format
  • Code builds successfully
  • Tests pass locally
  • Documentation updated (if needed)

Related Issues

If this is merged I fill follow up with also migrating the existing contact table

Tania Sanz added 4 commits May 31, 2026 11:37
…te for templates

Adopt @tanstack/react-table as the table framework and add a card/table
view switcher to the templates list, structured to shadcn data-table
conventions but built on the existing @plunk/ui primitives.

Backend
- Add a shared list-sort helper (apps/api/src/utils/listSort.ts) exposing
  a ListSort type + parseListSort() that validates ?sort=name|createdAt|
  updatedAt&dir=asc|desc against allow-lists, silently falling back to the
  existing createdAt desc default. Thread it through GET /templates,
  GET /workflows and GET /campaigns and their service list() methods into
  Prisma orderBy. Existing call shapes stay backward-compatible.
- Add POST /templates/bulk-update (TemplateSchemas.bulkUpdate) accepting
  { ids: string[] (1..1000), delete?: boolean }. Bulk delete runs the
  ownership/project-scope check (404 on a foreign id), the workflow-step
  reference check (409, mirroring single delete), and deleteMany inside one
  prisma.$transaction so a partial delete is impossible. The schema is kept
  open-ended for future bulk modes. Covers it with TemplateService tests.

Frontend (generic, reusable shared components under components/data-table)
- DataTable: presentation-only tanstack renderer on @plunk/ui Table, owns
  per-th aria-sort.
- DataTableColumnHeader: sortable header (asc -> desc -> unsorted) with
  chevron icons and an optional in-header filter slot.
- DataTableFacetedFilter: Excel-style per-column dropdown (Popover+Command)
  for fixed-value columns.
- DataTableViewOptions: column-visibility "Columns" selector.
- DataTableViewSwitcher: card/table icon toggle.
- BulkActionBar: selection action bar.
- hooks: useColumnVisibility (localStorage), usePersistentState (view),
  useShiftClickSelection (anchor+range selection).

Templates page
- Card view is the default and is unchanged from before (same search +
  Type filter pills + card grid). Table view adds header sort, a Type
  faceted filter in the column header (no pills, no sort-by dropdown), a
  localStorage-persisted column-visibility menu (Name + Actions locked),
  and a bulk-delete bar with shift-click range selection. Sorting and the
  Type facet both feed the SWR query string so the server stays
  authoritative (manualSorting). Selection clears on page/search/filter
  changes and after a successful delete. View and column choices persist in
  localStorage (plunk:templates:view, plunk:templates:columns).
…low status+steps, header casing

Address four review findings on the new shadcn data-table list-page UX.

1. No-results vs first-run empty state (templates/workflows/campaigns)
   Previously, when a search/facet filter matched zero rows, the page fell
   back to the first-run "No X yet" empty state and hid the table (and, in
   table view, its header facets) with no way to recover. Add a distinct
   "No results match your filters" state with a one-click "Clear filters"
   button (new components/data-table/NoResultsState) that resets search + all
   active facet/status filters + pagination. The first-run state now only
   shows when there are genuinely zero items AND no active filter. Each page
   derives a hasActiveFilters flag and a clearFilters handler from its
   existing filter state; the search bar / view switcher / card-view pills
   stay rendered above the empty state so manual recovery also remains
   reachable.

2. Bulk-action button sizing (BulkActionBar)
   Outline triggers read smaller than the solid "Delete selected" button.
   Pin every action button in the bar to a uniform height/padding
   ([&>button]:h-8 px-3 text-xs) so the bar renders as one consistent group
   regardless of variant border math.

3. Workflows: Status filter + Steps sorting
   - Status facet (Active / Disabled) on the Status column header, mirroring
     the campaigns Status facet, plus the equivalent card-view pill row.
     Wired to a new ?status=active|disabled query param the controller
     resolves to the enabled boolean in WorkflowService.list's Prisma where
     (covered by the existing @@index([projectId, enabled])).
   - Steps column is now sortable by step count. ?sort=steps maps onto
     Prisma orderBy: {steps: {_count}}; parseListSort gains an opt-in
     extraFields arg so steps is accepted for workflows without widening the
     shared allow-list. name/createdAt/updatedAt sorting unchanged.

4. Column header casing (DataTableColumnHeader)
   Sortable headers rendered Capitalized while plain <th> headers were
   UPPERCASE. Apply the same text-xs font-medium uppercase tracking-wider
   type styling to the sortable label in DataTableColumnHeader so every
   column header is uniform.

Also URL-encode the search query on templates/workflows lists (the campaigns
list already did this) so values containing special chars round-trip cleanly.

Tests: extend WorkflowService.list suite with enabled-status filtering and
step-count sorting (asc/desc) coverage.
@taniasanz7
Copy link
Copy Markdown
Contributor Author

@driaug did you get a chance to have a look?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant